package com.linqingying.cangjie.cjpm.toolchain.tools

import CjpmWorkspaceData
import com.fasterxml.jackson.core.JacksonException
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import com.intellij.execution.configuration.EnvironmentVariablesData
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.process.ProcessOutput
import com.intellij.execution.target.TargetEnvironmentRequest
import com.intellij.execution.target.TargetedCommandLineBuilder
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.registry.RegistryValue
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.net.HttpConfigurable
import com.intellij.util.text.SemVer
import com.linqingying.cangjie.cjpm.CjpmConstants
import com.linqingying.cangjie.cjpm.findChild
import com.linqingying.cangjie.cjpm.project.pathAsPath
import com.linqingying.cangjie.cjpm.resolve
import com.linqingying.cangjie.cjpm.toolchain.CjToolchainBase
import com.linqingying.cangjie.cjpm.toolchain.impl.CjpmMetadata
import com.linqingying.cangjie.cjpm.toolchain.parseSemVer
import com.linqingying.cangjie.ide.experiments.CjExperiments
import com.linqingying.cangjie.ide.project.tools.projectWizard.wizard.CjProcessResult
import com.linqingying.cangjie.ide.run.cjpm.CjpmCommandLine
import com.linqingying.cangjie.ide.run.cjpm.CjpmPatch
import com.linqingying.cangjie.ide.run.cjpm.runconfig.*
import com.linqingying.cangjie.ide.run.isFeatureEnabled
import com.linqingying.cangjie.lang.CjConstants.LIB_CJ_FILE
import com.linqingying.cangjie.lang.CjConstants.MAIN_CJ_FILE
import com.linqingying.cangjie.utils.buildList
import java.io.IOException
import java.nio.file.Path
import kotlin.io.path.exists


fun fullyRefreshDirectory(directory: VirtualFile) {
    VfsUtil.markDirtyAndRefresh(/* async = */ false, /* recursive = */ true, /* reloadChildren = */ true, directory)
}

//fun CjToolchainBase.cjpm(): Cjpm = Cjpm(this)
class Cjpm(
    toolchain: CjToolchainBase
) : CangJieComponent(NAME, toolchain) {

    init {
        toolchain.cjpm = this
    }

    fun checkNeedInstallCjpmGenerate(): Boolean {
        val crateName = "cjpm-generate"
        val minVersion = "0.53.4".parseSemVer()
        return checkBinaryCrateIsNotInstalled(crateName, minVersion)
    }

    private fun checkBinaryCrateIsNotInstalled(crateName: String, minVersion: SemVer?): Boolean {
        val installed = listInstalledBinaryCrates().any { (name, version) ->
            name == crateName && (minVersion == null || version != null && version >= minVersion)
        }
        return !installed
    }

    data class GeneratedFilesHolder(val manifest: VirtualFile, val sourceFiles: List<VirtualFile>)

    private fun listInstalledBinaryCrates(): List<BinaryCrate> =
        createBaseCommandLine("list")
            .execute(toolchain.executionTimeoutInMilliseconds)
            ?.stdoutLines
            ?.filterNot { it.startsWith(" ") }
            ?.mapNotNull { BinaryCrate.from(it) }
            .orEmpty()

    fun CjpmCommandLine.patchArgs(project: Project): CjpmCommandLine {

        val (pre, post) = splitOnDoubleDash()
            .let { (pre, post) -> pre.toMutableList() to post.toMutableList() }

//        TODO 对参数进行处理(添加或者删除)

        return copy(additionalArguments = if (post.isEmpty()) pre else pre + "--" + post)

    }


    fun init(
        project: Project,
        owner: Disposable,
        directory: VirtualFile,
        name: String,
        moduleName: String = name,
//        organizationName: String = name,
        projectType: String? = null,
//        cjcVersion: CjcVersion? = null,
    ): CjProcessResult<GeneratedFilesHolder> {
        val path = directory.pathAsPath
        val crateType = "--type=$projectType"
        val args = mutableListOf<String>()
//        val args = mutableListOf<String>(crateType,"--name=$moduleName")
        args.add(crateType)

//        val cjcVersion = toolchain.cjc().version
//
//        if (cjcVersion?.semver != null) {
//            if (cjcVersion.semver < SemVer.parseFromText("0.49.2")) {
//                args.add(moduleName)
//                args.add(moduleName)
//            } else {
//
//            }
//        } else {
//            args.add(moduleName)
//            args.add(moduleName)
//
//        }
        args.add("--name=$moduleName")


        CjpmCommandLine("init", path, args).execute(project, owner).unwrapOrElse { return CjResult.Err(it) }
        fullyRefreshDirectory(directory)

        val manifest =
            checkNotNull(directory.findChild(CjpmConstants.MANIFEST_FILE)) { "Can't find the manifest file" }


//        val manifest = checkNotNull(directory.findChild(CjpmConstants.MANIFEST_FILE)) { "Can't find the manifest file" }
        val fileName = MAIN_CJ_FILE
        val sourceFiles = listOfNotNull(directory.findFileByRelativePath("src/$fileName"))
        return CjResult.Ok(GeneratedFilesHolder(manifest, sourceFiles))
    }

    fun init(
        project: Project,
        owner: Disposable,
        directory: VirtualFile,
        name: String,
        createBinary: Boolean,
        vcs: String? = null
    ): CjProcessResult<GeneratedFilesHolder> {
        val path = directory.pathAsPath
        val crateType = "--type=${
            if (createBinary) "executable" else "static"
        }"

        val args = mutableListOf(crateType, name, name)
//
//        vcs?.let {
//            args.addAll(listOf("--vcs", vcs))
//        }

//        args.add(path.toString())

        CjpmCommandLine("init", path, args).execute(project, owner).unwrapOrElse { return CjResult.Err(it) }
        fullyRefreshDirectory(directory)

        val manifest = checkNotNull(directory.findChild(CjpmConstants.MANIFEST_FILE)) { "Can't find the manifest file" }
        val fileName = if (createBinary) MAIN_CJ_FILE else LIB_CJ_FILE
        val sourceFiles = listOfNotNull(directory.findFileByRelativePath("src/$fileName"))
        return CjResult.Ok(GeneratedFilesHolder(manifest, sourceFiles))
    }

    /**
     * 获取所有依赖项并计算项目信息
     */
    fun fullProjectDescription(
        owner: Project,
        projectDirectory: Path,


        listenerProvider: (CjpmCallType) -> ProcessListener? = { null }
    ): CjResult<CjpmWorkspaceData, CjProcessExecutionOrDeserializationException> {
        val rawData = fetchUpdate(owner, projectDirectory, listener = listenerProvider(CjpmCallType.METADATA))
            .unwrapOrElse { return CjResult.Err(it) }
//        return CjResult.Ok(ProjectDescription(workspaceData, status))
        val workspaceData = CjpmMetadata.clean(rawData)
        return CjResult.Ok(workspaceData)


    }

    /**
     * 对cjpm项目进行更新
     */
    fun fetchUpdate(
        owner: Project,
        projectDirectory: Path,

        toolchainOverride: String? = null,
        environmentVariables: EnvironmentVariablesData = EnvironmentVariablesData.DEFAULT,
        listener: ProcessListener?,

        ): CjResult<CjpmMetadata.Project, CjProcessExecutionOrDeserializationException> {

        val commandLine = CjpmCommandLine(
            command = "update",
            projectDirectory,
            toolchain = toolchainOverride,
            environmentVariables = environmentVariables
        )
        val output = commandLine.execute(
            owner, listener = listener
        ).unwrapOrElse { return CjResult.Err(it) }
        if (output.exitCode == 0 && output.stdout.contains("cjpm update success\n")) {
            try {
                val project = readFile(projectDirectory)
                return CjResult.Ok(project)
            } catch (e: JacksonException) {
                return CjResult.Err(CjDeserializationException(e))
            } catch (e: IOException) {
                return CjResult.Err(CjModuleNotFound())
            }
        }

        return CjResult.Err(CjProcessExecutionException.Canceled(commandLine.command, output))
//        return CjResult.Ok(CjpmMetadata.Project(1))
    }


    /**
     * 读取module-lock.json文件内容并序列化
     */
    private fun readFile(projectDirectory: Path): CjpmMetadata.Project {
        val path = projectDirectory.resolve(CjpmConstants.LOCK_FILE)
//        val path = projectDirectory.resolve(CjpmConstantsService(toolchain = toolchain).LOCK_FILE)

        if (path.exists()) {
            val project = CjpmMetadata.Project.serialization(path)
                .convertPaths(path.parent, toolchain::toLocalPath)
            return project


        }

        throw IOException("Can't find the module-lock.json file")
    }

    fun generate(
        project: Project,
        owner: Disposable,
        directory: VirtualFile,
        name: String,
        templateUrl: String,
        vcs: String? = null
    ): CjProcessResult<GeneratedFilesHolder> {
        val path = directory.pathAsPath
        val args = mutableListOf(
            "--name", name,
            "--git", templateUrl,
            "--init",
            "--force"
        )

        vcs?.let {
            args.addAll(listOf("--vcs", vcs))
        }

        CjpmCommandLine("generate", path, args)
            .execute(project, owner)
            .unwrapOrElse { return CjResult.Err(it) }
        fullyRefreshDirectory(directory)

        val manifest = checkNotNull(directory.findChild(CjpmConstants.MANIFEST_FILE)) { "Can't find the manifest file" }
        val sourceFiles = listOf("main", "lib").mapNotNull { directory.findFileByRelativePath("src/${it}.cj") }
        return CjResult.Ok(GeneratedFilesHolder(manifest, sourceFiles))
    }


    private fun CjpmCommandLine.execute(
        project: Project,
        owner: Disposable = project,
        stdIn: ByteArray? = null,
        listener: ProcessListener? = null
    ): CjProcessResult<ProcessOutput> {
        return toGeneralCommandLine(project, copy(emulateTerminal = false)).execute(owner, stdIn, listener = listener)
    }

    fun toTargetedCommandLineBuilder(
        project: Project,
        request: TargetEnvironmentRequest,
        commandLine: CjpmCommandLine
    ): TargetedCommandLineBuilder =
        with(
            commandLine.patchArgs(project)
        ) {

            val parameters = buildList {
                when {

                    toolchain != null -> add("+$toolchain")
                }

                add(command)
                addAll(additionalArguments)
            }
            val build = TargetedCommandLineBuilder(request)
            build.setExePath(executable.toString())
//            build.setExePath("D:\\Code\\cangjietest\\untitled3\\target\\release\\bin\\untitled1.exe")
            build.setWorkingDirectory(workingDirectory.toString())
//            build.setInputFile(TargetValue.fixed(redirectInputFrom?.path.toString()))
            build.addParameters(parameters)
            this@Cjpm.toolchain.getEnvironment().forEach { (s, s2) ->
                build.addEnvironmentVariable(s, s2)
            }
            environmentVariables.envs.forEach { (s, s2) ->
                build.addEnvironmentVariable(s, s2)
            }
            return build
        }

    fun toGeneralCommandLine(project: Project, commandLine: CjpmCommandLine): GeneralCommandLine =
        with(commandLine.patchArgs(project)) {
            val parameters = buildList {
                when {

                    toolchain != null -> add("+$toolchain")
                }

                add(command)
                addAll(additionalArguments)
            }


//            val cjcExecutable = this@Cjpm.toolchain.cjc().executable.toString()
            this@Cjpm.toolchain.createGeneralCommandLine(
                executable,
//                "D:\\Code\\cangjietest\\untitled3\\target\\release\\bin\\untitled1.exe".toPath(),
                workingDirectory,
                redirectInputFrom,

                environmentVariables,
                parameters,
//                emulateTerminal,
//                true,
              if (parameters.first() == "run") true else emulateTerminal,

                if (isFeatureEnabled(CjExperiments.BUILD_TOOL_WINDOW)) withSudo else false,
                http = http
            )


//            C:\Users\27439\.jdks\openjdk-21.0.2\bin\java.exe -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath D:\Code\test\untitled2\out\production\untitled2;D:\Maven\repository\org\jetbrains\kotlin\kotlin-stdlib\1.9.22\kotlin-stdlib-1.9.22.jar;D:\Maven\repository\org\jetbrains\annotations\13.0\annotations-13.0.jar AKt


        }

    private var _http: HttpConfigurable? = null

    private val http: HttpConfigurable
        get() = _http ?: HttpConfigurable.getInstance()

    fun checkSupportForBuildCheckAllTargets(): Boolean {
        val lines = createBaseCommandLine("help", "check")
            .execute(toolchain.executionTimeoutInMilliseconds)
            ?.stdoutLines
            ?: return false
        return lines.any { it.contains(" --all-targets ") }
    }

    data class BinaryCrate(val name: String, val version: SemVer? = null) {
        companion object {

            private val VERSION_LINE: Regex = """(?<name>[\w-]+) v(?<version>\d+\.\d+\.\d+(-[\w.]+)?).*""".toRegex()

            fun from(line: String): BinaryCrate? {
                val result = VERSION_LINE.matchEntire(line) ?: return null
                val name = result.groups["name"]?.value ?: return null
                val rawVersion = result.groups["version"]?.value ?: return null
                return BinaryCrate(name, SemVer.parseFromText(rawVersion))
            }
        }
    }

    companion object {
        val JSON_MAPPER: ObjectMapper = ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .registerKotlinModule()

        //        val TOML_MAPPER: ObjectMapper = TomlMapper().configure(TomlReadFeature.PARSE_JAVA_TIME, false)
//            .registerKotlinModule()
        const val NAME: String = "tools/bin/cjpm"

        @JvmStatic
        val TEST_NOCAPTURE_ENABLED_KEY: RegistryValue = Registry.get("com.linqingying.cangjie.cjpm.test.nocapture")
        private val FEATURES_ACCEPTING_COMMANDS: List<String> = listOf(
            "update", "build", "check", "run-script", "clean", "run", "test", "publish", "list", "load", "init", "help"
        )

        fun getCjpmCommonPatch(project: Project): CjpmPatch = { it.patchArgs(project, true) }
        fun CjpmCommandLine.patchArgs(project: Project, colors: Boolean): CjpmCommandLine {
            val (pre, post) = splitOnDoubleDash()
                .let { (pre, post) -> pre.toMutableList() to post.toMutableList() }

//            if (command in listOf("test", "bench")) {
//                if (allFeatures && !pre.contains("--all-features")) {
//                    pre.add("--all-features")
//                }
//                if (TEST_NOCAPTURE_ENABLED_KEY.asBoolean() && !post.contains("--nocapture")) {
//                    post.add(0, "--nocapture")
//                }
//            }

//            if (requiredFeatures && command in FEATURES_ACCEPTING_COMMANDS) {
//                run {
//                    val cjpmProject = findCjpmProject(project, additionalArguments, workingDirectory) ?: return@run
//                    val cjpmPackage = findCjpmPackage(cjpmProject, additionalArguments, workingDirectory)
//                        ?: return@run
//                    if (workingDirectory != cjpmPackage.rootDirectory) {
//                        val manifestIdx = pre.indexOf("--manifest-path")
//                        val packageIdx = pre.indexOf("--package")
//                        if (manifestIdx == -1 && packageIdx != -1) {
//                            pre.removeAt(packageIdx) // remove `--package`
//                            pre.removeAt(packageIdx) // remove package name
//                            pre.add("--manifest-path")
//                            val manifest = cjpmPackage.rootDirectory.resolve(CjpmConstants.MANIFEST_FILE)
//                            pre.add(manifest.toAbsolutePath().toString())
//                        }
//                    }
//                    val cjpmTargets = findCjpmTargets(cjpmPackage, additionalArguments)
//                    val features = cjpmTargets.flatMap { it.requiredFeatures }.distinct().joinToString(",")
//                    if (features.isNotEmpty()) pre.add("--features=$features")
//                }
//            }

//            // Force colors
//            val forceColors = colors &&
//                    command in COLOR_ACCEPTING_COMMANDS &&
//                    additionalArguments.none { it.startsWith("--color") }
//            if (forceColors) pre.add(0, "--color=always")

            return copy(additionalArguments = if (post.isEmpty()) pre else pre + "--" + post)
        }
    }

}


inline fun <T, E> CjResult<T, E>.unwrapOrElse(op: (E) -> T): T = when (this) {
    is CjResult.Ok -> ok
    is CjResult.Err -> op(err)
}

fun GeneralCommandLine.execute(
    owner: Disposable,
    stdIn: ByteArray? = null,
    runner: CapturingProcessHandler.() -> ProcessOutput = { runProcessWithGlobalProgress(timeoutInMilliseconds = null) },
    listener: ProcessListener? = null
): CjProcessResult<ProcessOutput> {


    val handler = CjCapturingProcessHandler.startProcess(this) // The OS process is started here
        .unwrapOrElse {

            return CjResult.Err(CjProcessExecutionException.Start(commandLineString, it))
        }

    val cjpmKiller = Disposable {

        if (!handler.isProcessTerminated) {
            handler.process.destroyForcibly() // Send SIGKILL
            handler.destroyProcess()
        }
    }

    val alreadyDisposed = runReadAction {
        if (Disposer.isDisposed(owner)) {
            true
        } else {
            Disposer.register(owner, cjpmKiller)
            false
        }
    }

    if (alreadyDisposed) {
        Disposer.dispose(cjpmKiller) // Kill the process

        val output = ProcessOutput().apply { setCancelled() }
        return CjResult.Err(CjProcessExecutionException.Canceled(commandLineString, output, "Command failed to start"))
    }

    listener?.let { handler.addProcessListener(it) }

    val output = try {
        if (stdIn != null) {
            handler.processInput.use { it.write(stdIn) }
        }

        handler.runner()
    } finally {
        Disposer.dispose(cjpmKiller)
    }

    return when {
        output.isCancelled -> CjResult.Err(CjProcessExecutionException.Canceled(commandLineString, output))
        output.isTimeout -> CjResult.Err(CjProcessExecutionException.Timeout(commandLineString, output))
        output.exitCode != 0 -> CjResult.Err(CjProcessExecutionException.ProcessAborted(commandLineString, output))
        else -> CjResult.Ok(output)
    }
}

enum class CjpmCallType {
    METADATA,
    BUILD_SCRIPT_CHECK
}

enum class ProjectDescriptionStatus {
    BUILD_SCRIPT_EVALUATION_ERROR,
    OK
}

data class ProjectDescription(
    val workspaceData: CjpmWorkspaceData,
    val status: ProjectDescriptionStatus
)


